feat: Add configurable automatic retries to the HTTP client#1659
Conversation
Co-Authored-By: adam <adam@workos.com>
Original prompt from adam
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Greptile SummaryThis PR brings the Node SDK's retry behavior to parity with the Kotlin, Go, Ruby, and Python SDKs: retries now apply to all requests (not just two path prefixes),
Confidence Score: 5/5Safe to merge — the retry machinery is well-tested, the backoff and jitter logic is correct, and the Retry-After and idempotency-key handling matches the behaviour of other WorkOS SDKs. The implementation correctly handles the full retry lifecycle: retryable statuses, non-JSON error bodies, Retry-After header parsing (both delay-seconds and HTTP-date), idempotency key injection for POST, and configurable limits at both client and request level. The test suite is thorough and all edge cases are covered. The only gap is that per-request maxRetries cannot be passed to WorkOS.delete and deleteWithBody due to those methods existing signatures, but the client-wide setting still applies to deletes. src/workos.ts — the delete and deleteWithBody methods do not propagate a per-request maxRetries override; all other files look correct. Important Files Changed
|
Co-Authored-By: adam <adam@workos.com>
|
@greptileai re: "Retry before parsing" (
|
|
The fix looks correct and well-reasoned.
One minor observation: The |
|
Update: I ended up fixing this rather than leaving it. As of 9c71ffa, |
Co-Authored-By: adam <adam@workos.com>
- Restrict automatic Idempotency-Key injection to POST requests, matching the Kotlin and Go SDKs and the API, which honors the header on create (POST) endpoints. PUT/PATCH no longer get an auto-generated key. - Cap server-provided Retry-After delays at 60 seconds so a large delay-seconds value or far-future HTTP-date can't hang callers. - Parse Retry-After delay-seconds strictly per RFC 9110, rejecting exotic numeric forms like Infinity, hex, and exponent notation. - Document automatic retries and maxRetries configuration in the README. - Add tests: HTTP-date Retry-After, 60s cap, unparseable-value fallback, network-error retry, DELETE retry, and no auto key on PUT/PATCH. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gjtorikian
left a comment
There was a problem hiding this comment.
i pushed a few changes, looks good to me!
Summary
Brings the Node SDK's automatic retry behavior to parity with the Kotlin, Go, Ruby, and Python SDKs.
Previously the Node SDK had retry machinery, but it was effectively dormant: retries only fired for two path prefixes (
/vault/*and/audit_logs/events), never retried429, ignoredRetry-After, added no idempotency safety for retried writes, and weren't configurable. Every other WorkOS SDK retries all requests on transient failures with backoff + jitter.This PR makes retries apply to all requests and configurable, while keeping default behavior transparent to existing callers (default is still 3 attempts, backoff + jitter unchanged).
What changed
Retries apply to every request, not just the two allowlisted path prefixes. The per-verb
isPathRetryable(path)branch is removed; all verbs go throughfetchRequestWithRetry.Retryable statuses now include
429and503(was[408, 500, 502, 504], now[408, 429, 500, 502, 503, 504]). Transport errors (TypeError) are still retried.Retry-Afteris honored, capped at 60 seconds. When a retryable response carries aRetry-Afterheader, the SDK sleeps for that duration instead of the computed backoff — capped atMAXIMUM_RETRY_AFTER_TIME_IN_MILLISECONDS = 60_000so a large server-provided delay can't hang callers. Supports both delay-seconds (strict RFC 9110 integer) and HTTP-date forms:Idempotency-Key auto-injection for retried
POSTrequests. A generated key (retry-<uuid>) is attached once toPOSTrequests that don't already have one, and reused across attempts so a retried write is not applied twice. A caller-provided key is always preserved. POST-only to match the Kotlin and Go SDKs and the API, which honorsIdempotency-Keyon create (POST) endpoints.Backoff is now capped (
MAXIMUM_SLEEP_TIME_IN_MILLISECONDS = 8_000) to avoid unbounded delays, matching Kotlin'smaxDelayMs.Configurable
maxRetries, client-wide and per-request,0disables retries:Wired through
WorkOSOptions, the per-call option interfaces (GetOptions/PostOptions/PutOptions/PatchOptions), andRequestOptions. Per-request wins over client-wide, which falls back to the default of 3.Notes
408/429/5xxare now retried by default, a couple of existing error-mapping tests that asserted a single terminal500/429response now construct their client withmaxRetries: 0to isolate the mapping from retry behavior.Test plan
src/common/net/fetch-client.spec.ts: retries on non-allowlisted paths,429,Retry-Afterhonored (delay-seconds and HTTP-date, 60s cap, unparseable fallback), idempotency key generated + reused on POST (and not on PUT/PATCH), caller key preserved, network-error retry, DELETE retry,maxRetries: 0disables, per-request override.npm run lint,npm run typecheck,npm test(888 passing), andnpm run buildall pass locally.maxRetriesconfiguration.Link to Devin session: https://app.devin.ai/sessions/45217aff3c0248e083c27dd69b391ca5
Requested by: @awolfden